package com.example.framework.operatelog.core.aop;

import cn.hutool.core.date.StopWatch;
import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.servlet.JakartaServletUtil;
import cn.hutool.json.JSONUtil;
import com.example.framework.common.event.OperationLogEvent;
import com.example.framework.common.pojo.R;
import com.example.framework.common.util.json.JsonUtils;
import com.example.framework.common.util.thread.ThreadUtils;
import com.example.framework.mq.kafka.core.util.KafkaProducerUtils;
import com.example.framework.operatelog.core.annotation.OperationLog;
import com.example.framework.security.core.util.SecurityFrameworkUtils;
import com.example.framework.web.core.handler.GlobalExceptionHandler;
import com.example.framework.web.core.util.WebFrameworkUtils;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;

import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;

@Slf4j
@Aspect
public class OperationLogAspect {

    @Value("${spring.application.name}")
    private String applicationName;

    @Resource
    private GlobalExceptionHandler globalExceptionHandler;

    @Value("${operation.mqTopic}")
    private String mqTopic;

    private final ThreadLocal<StopWatch> stopWatch = ThreadLocal.withInitial(() -> StopWatch.create(Thread.currentThread().getName()));

    @Pointcut("@annotation(com.lexar.common.security.core.annotations.OperationLog)")
    private void operationLog() {
    }

    @Before("operationLog()")
    // 不能直接在方法签名中接收 LogRequest 参数。这是因为 @within 表达式用于指定切点，而不是直接获取注解实例。
    public void before(JoinPoint joinPoint) {
        stopWatch.get().start();
    }

    @AfterReturning(pointcut = "@annotation(operationLog)", returning = "jsonResult") // 多个 @AfterReturning 也只会执行一次
    public void doAfterReturning(JoinPoint joinPoint, OperationLog operationLog, Object jsonResult) {
        log.debug("joinPoint: {}", joinPoint);
        handleLog(joinPoint, jsonResult);
    }

    @AfterThrowing(value = "@annotation(operationLog)", throwing = "e")
    public void doAfterThrowing(JoinPoint joinPoint, OperationLog operationLog, Exception e) {
        log.error("joinPoint:{}", joinPoint);
        handleLog(joinPoint, globalExceptionHandler.allExceptionHandler(e));
    }

    private void handleLog(JoinPoint joinPoint, Object jsonResult) {
        String ip;
        String path;
        HttpServletRequest httpServletRequest = WebFrameworkUtils.getRequest();
        if (Objects.nonNull(httpServletRequest)) {
            ip = JakartaServletUtil.getClientIP(httpServletRequest);
            path = httpServletRequest.getRequestURL().toString();
        } else {
            ip = null;
            path = null;
        }

        stopWatch.get().stop();
        long costMillis = stopWatch.get().getLastTaskTimeMillis();

        ThreadUtils.execute(() -> publishOperationLogEvent(joinPoint, costMillis, jsonResult, ip, path));

        // 清理 ThreadLocal，虽然这里只用最后一次任务时间，但是长期下来会一直记录任务数据占用内存
        stopWatch.remove();
    }

    /**
     * 发布操作日志事件
     */
    private void publishOperationLogEvent(JoinPoint joinPoint, Long costTime, Object result, String ip, String path) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();

        Map<String, Object> params = new HashMap<>();
        String[] parameterNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();
        if (Objects.nonNull(parameterNames)) {
            for (int i = 0; i < parameterNames.length; i++) {
                params.put(parameterNames[i], args[i]);
            }
        }

        OperationLogEvent operationLogEvent = new OperationLogEvent()
                .setUserId(SecurityFrameworkUtils.getLoginUserId())
                // operation优先取注解里的值，没有再从swagger的注解取
                .setOperation(Optional.of(method.getAnnotation(OperationLog.class).value())
                        .filter(StrUtil::isNotEmpty)
                        .orElseGet(() -> Optional.ofNullable(method.getAnnotation(Operation.class))
                                .map(Operation::summary)
                                .filter(StrUtil::isNotEmpty)
                                .orElse(null)))
                .setApplicationName(applicationName)
                .setMethod(joinPoint.getTarget().getClass().getName() + "." + signature.getMethod().getName() + "()")
                // 不能用工具类里的jackson，有的请求参数一次性读取的，会报错：getOutputStream() has already been called for this response，用hutool的就算是文件也不会报错
                .setParams(JSONUtil.toJsonStr(params))
                .setCostTime(costTime)
                .setIp(ip)
                .setPath(path);
        if (result instanceof R<?> baseResponse) {
            operationLogEvent
                    .setTraceId(baseResponse.getLogId())
                    .setRetCode(baseResponse.getCode())
                    .setResult(baseResponse.isSuccess() ? JsonUtils.toJsonString(baseResponse.getData()) : baseResponse.getMsg());
        }

        log.debug("发布事件：{}", operationLogEvent);
        KafkaProducerUtils.sendMessageAsync(mqTopic, operationLogEvent);
    }

}
